Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM]: Inferred types for aggregations #37360

Merged
merged 6 commits into from
Jun 17, 2019

Conversation

dgieselaar
Copy link
Member

@dgieselaar dgieselaar commented May 29, 2019

Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.

This change infers the aggregation return type from the parameters passed to ESClient.search.

e.g. from the following parameters:

const params = {
  body: {
    aggs: {
      by_interval: {
        date_histogram: {
          field: '@timestamp'
        },
        aggs: {
          by_service_name: {
            terms: {
              field: 'service_name.keyword'
            }
          }
        }
      }
    }
  }
};

it will infer the return type (approximately):

const response = {
  aggregations: {
    by_interval: {
      buckets: Array<{
        doc_count: number;
        key: number;
        key_as_string: string;
        by_service_name: {
          buckets: Array<{
            doc_count:number;
            key: string;
            key_as_string:string;
          }>
        }
      }>
    }
  }
}	

Here's a TypeScript REPL.

@dgieselaar dgieselaar requested a review from a team as a code owner May 29, 2019 17:04
@@ -37,7 +36,10 @@ export async function getErrorGroups({
}) {
const { start, end, uiFiltersES, client, config } = setup;

const params: SearchParams = {
// sort buckets by last occurrence of error
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS was complaining about changing the shape of the object after creating it, so I moved it inline.

@@ -22,14 +22,20 @@ const getRelativeImpact = (
);

function getWithRelativeImpact(items: TransactionListAPIResponse) {
const impacts = items.map(({ impact }) => impact);
const impacts = items
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values from metric aggregations can be null too

@@ -52,6 +50,9 @@ const chartBase: ChartBase<HeapMemoryMetrics> = {
};

export async function getHeapMemoryChart(setup: Setup, serviceName: string) {
const result = await fetch(setup, serviceName);
return transformDataToMetricsChart<HeapMemoryMetrics>(result, chartBase);
const result = (await fetch(setup, serviceName)) as MetricSearchResponse<
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't explicitly tell which keys from aggregations are related to the chart, TS will create a union type of date_histogram and min/max buckets, and TS does not allow (AFAICT) maps over union types: microsoft/TypeScript#29011 (comment)

CpuSearchResponse,
'aggregations'
> & {
aggregations: IndexAsString<
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes TS needs a little help in realising object keys are strings (something to do with the keyof operator).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can understand it...since numbers and symbols are valid object keys too, but in our case, it will always be strings :)

@@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({
};

try {
return await client<void, Aggs>('search', params);
const response = await client<void, typeof params>('search', params);
return response;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's kind of weird to me that this is in a try/catch and silently fails when there is a HttpError. Anyone who knows why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't know why it's like this, but i agree it seems strange. I would imagine that it might be this way so that a chart is simply omitted from rendering when there is an error, rather than displaying a chart + error messaging. But it seems like this is incomplete UX.

lower: number;
};
};
}[AggregationType & keyof AggregationOption[AggregationName]]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AggregationOption:

{
	name_of_agg: {
		terms: {
			...
		}
	}
}

That needs to be translated to:

{
	name_of_agg: {
		buckets: []
	}
}

That's what this part is for. It picks whatever key from name_of_agg that is an aggregation type, and uses that to get the response type from the parent object. Dirty, but works.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@dgieselaar dgieselaar requested review from ogupte and sorenlouv May 31, 2019 07:11
@dgieselaar dgieselaar force-pushed the aggregation-types branch from 142e695 to d0cf61f Compare May 31, 2019 10:26
@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@dgieselaar dgieselaar force-pushed the aggregation-types branch 2 times, most recently from 50c1548 to a5d4f1d Compare June 13, 2019 13:24
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@dgieselaar dgieselaar added the release_note:skip Skip the PR/issue when compiling release notes label Jun 13, 2019
Copy link
Contributor

@ogupte ogupte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really great stuff. takes out much of the boilerplate type definitions, and fixes inconsistencies with ES types. Just a few comments I left below.

@@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({
};

try {
return await client<void, Aggs>('search', params);
const response = await client<void, typeof params>('search', params);
return response;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't know why it's like this, but i agree it seems strange. I would imagine that it might be this way so that a chart is simply omitted from rendering when there is an error, rather than displaying a chart + error messaging. But it seems like this is incomplete UX.

keyof typeof chartBase.series
>;

return transformDataToMetricsChart(result, chartBase as ChartBase);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type assertions could be obscuring type errors in the value. For chartBase, i like how we no longer have to define a special interface for chart-specific metrics. but it's now possible that a type mismatch for the rest of chartBase makes it thru to transformDataToMetricsChart via the type assertion. It might be safer for chartBase to be typed when it's defined.

A possible solution for best of both worlds:

const chartBaseSeries = {
  heapMemoryUsed: {
    title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesUsed', {
      defaultMessage: 'Avg. used'
    }),
    color: theme.euiColorVis0
  },
  heapMemoryCommitted: {
    title: i18n.translate(
      'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted',
      {
        defaultMessage: 'Avg. committed'
      }
    ),
    color: theme.euiColorVis1
  },
  heapMemoryMax: {
    title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', {
      defaultMessage: 'Avg. limit'
    }),
    color: theme.euiColorVis2
  }
};

const chartBase: ChartBase = {
  title: i18n.translate('xpack.apm.agentMetrics.java.heapMemoryChartTitle', {
    defaultMessage: 'Heap Memory'
  }),
  key: 'heap_memory_area_chart',
  type: 'area',
  yUnit: 'bytes',
  series: chartBaseSeries
};

export async function getHeapMemoryChart(setup: Setup, serviceName: string) {
  const result = await fetch<keyof typeof chartBaseSeries>(setup, serviceName);

  return transformDataToMetricsChart(result, chartBase);
}

with fetch defined as type:

export async function fetch<ChartBaseSeriesKey extends string>(
  setup: Setup,
  serviceName: string
): Promise<MetricSearchResponse<ChartBaseSeriesKey>> {
  ...

Copy link
Member Author

@dgieselaar dgieselaar Jun 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah definitely looks better! I tried to make this work but ran into some issues when casting the return value to the return type. I've landed on a solution where instead of having several fetchers and fetch calls, we have a generic fetchAndTransformMetrics call with some parameters. fetchAndTransformMetrics then calls transformDataToMetricsChart, and TypeScript then - for some reason - is able to match the types together. Bonus is that we can merge a bunch of largely overlapping fetcher files into one file.

CpuSearchResponse,
'aggregations'
> & {
aggregations: IndexAsString<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can understand it...since numbers and symbols are valid object keys too, but in our case, it will always be strings :)

const values = transactionGroups.map(({ impact }) => impact);
const values = transactionGroups
.map(({ impact }) => impact)
.filter(value => value !== null) as number[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the type assertion as number[] is not needed here since it's inferred correctly

Copy link
Member Author

@dgieselaar dgieselaar Jun 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impact has a union type of number and null, and TypeScript can't figure out that we filtered out all non-null values in the filter function before it. I think in Lodash 4.x you can use compact to achieve the same effect, but the typings for 3.x are not complete enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@@ -27,7 +34,7 @@ function getTransactionGroup(
const averageResponseTime = bucket.avg.value;
const transactionsPerMinute = bucket.doc_count / minutes;
const impact = bucket.sum.value;
const sample = bucket.sample.hits.hits[0]._source;
const sample = bucket.sample.hits.hits[0]._source as Transaction;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type assertion as Transaction should not be needed here since it inferred from _source

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for the top-level hits object, but this is from a top_hits aggregation, so it needs a type hint. I'm not sure how to solve it without the type hint because it's so dynamic. Do you have any ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case, i think you're right to use the type assertion

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@elasticmachine
Copy link
Contributor

💔 Build Failed

@dgieselaar
Copy link
Member Author

retest

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.

This change infers the aggregation return type from the parameters passed to ESClient.search.
@dgieselaar dgieselaar requested a review from ogupte June 17, 2019 07:14
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Contributor

@ogupte ogupte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@dgieselaar dgieselaar merged commit 4526e2a into elastic:master Jun 17, 2019
@dgieselaar dgieselaar deleted the aggregation-types branch June 17, 2019 19:56
dgieselaar added a commit to dgieselaar/kibana that referenced this pull request Jun 18, 2019
* [APM]: Inferred types for aggregations

Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.

This change infers the aggregation return type from the parameters passed to ESClient.search.

* Fix idx error

* Safeguard against querying against non-existing indices in functional tests

* Improve metric typings

* Automatically infer params from function arguments

* Remove unnecessary type hints
dgieselaar added a commit that referenced this pull request Jun 18, 2019
* [APM]: Inferred types for aggregations

Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.

This change infers the aggregation return type from the parameters passed to ESClient.search.

* Fix idx error

* Safeguard against querying against non-existing indices in functional tests

* Improve metric typings

* Automatically infer params from function arguments

* Remove unnecessary type hints
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release_note:skip Skip the PR/issue when compiling release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants